package modelo;

import java.util.EmptyStackException;
import java.util.Scanner;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

enum Tipo
{

    NINGUNO, NUMERO, OPERADOR, PARENTESIS_IZQ, PARENTESIS_DER, FUNCION, SEPARADOR
}

public class Expresion
{

    private Pattern numRegex = Pattern.compile("([0-9]*[.]?[0-9]+[eE]{1}-?[0-9]+)"
                                             + "|([0-9]+[.]?[0-9]*[eE]{1}-?[0-9]+)"
                                             + "|([0-9]*[.]?[0-9]+)" + "|([0-9]+)");
    
    private Pattern operRegex = Pattern.compile("([\\Q()+--*/%^\\E]{1})");
    private Pattern funcRegex = Pattern.compile("[a-zA-Z]+[_1-9a-zA-Z]*");
    
    
    // orden de operaciones (a mayor indice, mayor la precedencia. igual indice = igual precedencia
    private String[] ordenDeOperaciones = {"bl+ bl-", "bl* bl/ bl%", "un-", "br^"};
    
    private Stack<Object> outputStack;
    private Stack<Object> operadoresStack;
    private Object tokenActual;
    private Object tokenPrevio;
    private Matcher numMatcher;
    private Matcher operMatcher;
    private Matcher funcMatcher;
    private String ecuacion;
    private int contador;
    private boolean analizado;

    public Expresion(String ecuacion)
    {
        this.ecuacion = ecuacion;
        numMatcher = numRegex.matcher(ecuacion);
        operMatcher = operRegex.matcher(ecuacion);
        funcMatcher = funcRegex.matcher(ecuacion);
        analizarExpresion();
        analizado = true;
    }

    /**
     * construye una pila RPN (notacion en reversa polaca) para esta expresion.
     */
    private void analizarExpresion()
    {
        contador = 0;
        tokenActual = null;
        tokenPrevio = null;
        outputStack = new Stack<Object>();
        operadoresStack = new Stack<Object>();

        while (contador < ecuacion.length())
        {
            Tipo leido = getSiguiente();

            switch (leido)
            {
                case NUMERO:
                    outputStack.push(tokenActual);
                    break;

                case FUNCION:
                    operadoresStack.push(tokenActual);
                    break;

                case SEPARADOR:
                    while (true)
                    {
                        if (operadoresStack.isEmpty())
                        {
                            throw new ErrorSintaxis("coma innecesaria");
                        }

                        Object temp = operadoresStack.pop();
                        leido = getTipo(temp);

                        if (leido == Tipo.PARENTESIS_IZQ)
                        {
                            Funcion funcion = (Funcion) operadoresStack.pop();
                            funcion.incrementarNumArgumentos();
                            operadoresStack.push(funcion);
                            operadoresStack.push(temp);
                            break;
                        }
                        outputStack.push(temp);
                    }
                    break;

                case OPERADOR:
                    while (operadoresStack.isEmpty() == false
                            && getTipo(operadoresStack.peek()) == Tipo.OPERADOR)
                    {
                        Operador top = (Operador) operadoresStack.peek();

                        if (evaluarPrecedencia((Operador) tokenActual, top))
                        {
                            outputStack.push(operadoresStack.pop());
                        } else
                        {
                            break;
                        }
                    }

                    operadoresStack.push(this.tokenActual);

                    break;

                case PARENTESIS_IZQ:
                    if (getTipo(this.tokenPrevio) == Tipo.FUNCION)
                    {
                        Funcion temp = (Funcion) this.operadoresStack.pop();
                        temp.setNumArgumentos(1);
                        this.operadoresStack.push(temp);
                    }

                    this.operadoresStack.push(this.tokenActual);
                    break;

                case PARENTESIS_DER:
                    try
                    {
                        while (getTipo(this.operadoresStack.peek()) != Tipo.PARENTESIS_IZQ)
                        {
                            this.outputStack.push(this.operadoresStack.pop());
                        }

                        this.operadoresStack.pop();

                        if (getTipo(this.operadoresStack.peek()) == Tipo.FUNCION)
                        {
                            outputStack.push(operadoresStack.pop());
                        }

                    } catch (EmptyStackException e)
                    {
                        // la pila esta vacia
                    }
                    break;

                default:
                    throw new ErrorSintaxis("expresion mal formada");
            }
        }

        while (!operadoresStack.empty())
        {
            // todavia existen operadoeres
            tokenActual = operadoresStack.pop();
            if (getTipo(tokenActual) == Tipo.PARENTESIS_IZQ)
            {
                // el parentesis izquierdo no coincide
                throw new ErrorSintaxis("parentesis izquierdo no coincide");
            }

            outputStack.push(tokenActual);
        }
    }

    /**
     * Determina el orden de operaciones entre op1 y op2
     *
     * @param op1
     * @param op2
     * @return true si:<br>
     * - si op1 es asociativo izquierdo y tiene una precedencia menor o igual
     * que op2<br>
     * - si op1 es asociativo derecho y tiene una precedencia menor que op2<br>
     * - si op1 no es asociativo y tiene menor precedencia que op2<br>
     * De otro modo retorna falso
     */
    private boolean evaluarPrecedencia(Operador op1, Operador op2)
    {
        int op1Prec = -1;
        int op2Prec = -1;
        for (int i = 0; i < ordenDeOperaciones.length; i++)
        {
            Scanner reader = new Scanner(ordenDeOperaciones[i]);
            while (reader.hasNext())
            {
                String comparacion = reader.next();

                if (comparacion.equals(op1.getSignoConFlags(true)))
                {
                    op1Prec = i;
                }

                if (comparacion.equals(op2.getSignoConFlags(true)))
                {
                    op2Prec = i;
                }

            }
            reader.close();
        }
        if (op1Prec == -1 || op2Prec == -1)
        {

            throw new ErrorSintaxis("expresion mal formada");
        }
        if (op1.esAsociativoIzq())
        {
            return op1Prec <= op2Prec ? true : false;

        } else if (op1.esAsociativoDerecha()) // TODO probar eliminar este if xq
        // es igual al else despues
        {
            return op1Prec < op2Prec ? true : false;

        } else
        {
            return op1Prec < op2Prec ? true : false;
        }
    }

    /**
     * coloca el elemento que esta en la posicion del contador en el token
     * actual
     *
     * @return el tipo de objeto del actual token
     */
    private Tipo getSiguiente()
    {
        while (Character.isWhitespace(ecuacion.charAt(contador)))
        {
            contador++;
        }

        tokenPrevio = tokenActual;
        if (ecuacion.charAt(contador) == '(')
        {
            tokenActual = "(";
            contador++;
            return Tipo.PARENTESIS_IZQ;
        } else if (ecuacion.charAt(contador) == ')')
        {
            tokenActual = ")";
            contador++;
            return Tipo.PARENTESIS_DER;
        } else if (ecuacion.charAt(contador) == ',')
        {
            // separador coma
            tokenActual = ",";
            contador++;
            return Tipo.SEPARADOR;
        } else if (funcMatcher.find(contador)
                && funcMatcher.start() == contador)
        {
            // una funcion
            tokenActual = new Funcion(funcMatcher.group());
            ((Funcion) tokenActual).setNumArgumentos(1);
            contador = funcMatcher.end();
            return Tipo.FUNCION;
        } else if (numMatcher.find(contador) && numMatcher.start() == contador)
        {
            // un numero
            tokenActual = Double.parseDouble(numMatcher.group());
            contador = numMatcher.end();
            return Tipo.NUMERO;
        } else if (operMatcher.find(contador)
                && operMatcher.start() == contador)
        {
            String flagsDeOperadores;
            if (getTipo(tokenPrevio) != Tipo.NUMERO
                    && getTipo(tokenPrevio) != Tipo.PARENTESIS_DER)
            {
                flagsDeOperadores = "u";
                for (int i = 0; i < ordenDeOperaciones.length; i++)
                {
                    Scanner scanner = new Scanner(ordenDeOperaciones[i]);
                    while (scanner.hasNext())
                    {
                        String temp = scanner.next();
                        if (temp.charAt(0) == 'u'
                                && temp.substring(2, temp.length()).equals(
                                operMatcher.group()))
                        {
                            flagsDeOperadores += temp.charAt(1);
                        }
                    }
                    scanner.close();
                }
            } else
            {
                flagsDeOperadores = "b";
                for (int i = 0; i < ordenDeOperaciones.length; i++)
                {
                    Scanner scanner = new Scanner(ordenDeOperaciones[i]);
                    while (scanner.hasNext())
                    {
                        String temp = scanner.next();
                        if (temp.charAt(0) == 'b'
                                && temp.substring(2, temp.length()).equals(
                                operMatcher.group()))
                        {
                            flagsDeOperadores += temp.charAt(1);
                        }
                    }

                    scanner.close();
                }
            }
            tokenActual = new Operador(flagsDeOperadores + operMatcher.group());
            contador = operMatcher.end();
            return Tipo.OPERADOR;
        } else
        {
            // token invalido
            throw new ErrorSintaxis("no existe el elemento" + tokenActual);
        }
    }

    private Tipo getTipo(Object obj)
    {
        if (obj instanceof Double)
        {
            return Tipo.NUMERO;
        } else if (obj instanceof Funcion)
        {
            return Tipo.FUNCION;
        } else if (obj instanceof Operador)
        {
            return Tipo.OPERADOR;
        } else if (obj instanceof String && obj.equals("("))
        {
            return Tipo.PARENTESIS_IZQ;
        } else if (obj instanceof String && obj.equals(")"))
        {
            return Tipo.PARENTESIS_DER;
        } else
        {
            return Tipo.NINGUNO;
        }
    }

    /**
     * Obtiene el resultado de evaluar la expresion. Solo si ha sido analizada.
     *
     * @return El resultado de evaluar la expresion.
     */
    public double evaluarExpresion()
    {
        if (!analizado)
        {
            throw new ErrorSintaxis("esta expresion es incoherente");
        }

        @SuppressWarnings("unchecked")
        Stack<Object> temp = (Stack<Object>) outputStack.clone();
        double res = eval();
        outputStack = temp;
        return res;
    }

    /**
     * Metodo auxiliar para evaluar la expresion
     */
    private double eval()
    {
        Object obtenido;
        while (!outputStack.isEmpty())
        {
            obtenido = outputStack.pop();
            if (getTipo(obtenido) == Tipo.OPERADOR)
            {
                if (((Operador) obtenido).esUnario())
                {
                    double[] operandos =
                    {
                        this.eval()
                    };
                    return ((Operador) obtenido).evaluar(operandos);
                } else if (((Operador) obtenido).esBinario())
                {
                    double temp = this.eval();
                    double[] args =
                    {
                        this.eval(), temp
                    };
                    return ((Operador) obtenido).evaluar(args);
                } else
                {
                    throw new ErrorSintaxis("expresion mal formada");
                }
            } else if (getTipo(obtenido) == Tipo.FUNCION)
            {
                double[] args = new double[((Funcion) obtenido).getNumArgumentos()];
                for (int i = args.length - 1; i >= 0; i--)
                {
                    args[i] = this.eval();
                }
                return ((Funcion) obtenido).evaluar(args);

            } else if (getTipo(obtenido) == Tipo.NUMERO)
            {
                return ((Double) obtenido).doubleValue();
            }
        }
        throw new ErrorSintaxis("expresion mal formada");
    }
}